package com.scs.application.modules.sys.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.scs.application.consts.GlobalConsts;
import com.scs.application.core.exception.BusinessException;
import com.scs.application.core.query.QueryContext;
import com.scs.application.core.service.BaseServiceImpl;
import com.scs.application.core.utils.*;
import com.scs.application.modules.base.util.CommonService;
import com.scs.application.modules.sys.entity.Log;
import com.scs.application.modules.sys.entity.ScheduleJob;
import com.scs.application.modules.sys.enums.LogType;
import com.scs.application.modules.sys.mapper.ScheduleJobMapper;
import com.scs.application.modules.sys.mapper.ScheduleLogMapper;
import com.scs.application.modules.sys.model.RestResult;
import com.scs.application.modules.sys.schedule.SpringBeanMethodInvokeTask;
import com.scs.application.modules.sys.schedule.StaticMethodInvokeTask;
import com.scs.application.modules.sys.service.LogService;
import com.scs.application.modules.sys.service.ScheduleJobService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.SchedulingException;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 定时任务 服务实现类
 * </p>
 *
 */
@Slf4j
@Service
public class ScheduleJobServiceImpl extends BaseServiceImpl<ScheduleJobMapper, ScheduleJob> implements ScheduleJobService{

    @Lazy
    @Autowired
    private SchedulerFactoryBean scheduler;

    @Autowired
    @Lazy
    private ScheduleLogMapper scheduleLogMapper;
    @Autowired
    @Lazy
    protected LogService logService;

    @Override
    public Page<?> page(QueryContext context) {
        Page<ScheduleJob> page = (Page<ScheduleJob>) super.page(context);
        List<ScheduleJob> records = page.getRecords();
        return page;
    }

    @Override
    public boolean saveOrUpdate(ScheduleJob entity) {
        CronTriggerImpl cronTriggerImpl = new CronTriggerImpl();
        try {
            cronTriggerImpl.setCronExpression(entity.getCronExpression());
        } catch(ParseException e) {
            log.error("saveOrUpdate.error 周期表达式格式错误，请重新选择",e);
            BusinessException.throwValidFail("周期表达式格式错误，请重新选择,原因："+e.getMessage());
        }


        if (StringUtils.isBlank(entity.getId())) {
            this.save(entity);
        }else {
            ScheduleJob entityDb=this.getById(entity.getId());
            //任务执行中禁止修改
            if (GlobalConsts.JobStatusExecution.EXEC.equals(entityDb.getStatusExecution()))  throw new BusinessException("[%s]修改失败，该任务已在执行中，请等待任务结束再进行修改",entity.getJobName());

            this.updateById(entity);

            if (!GlobalConsts.JobStatusExecution.NOT_RUN.equals(entityDb.getStatusExecution()) || !entity.getFlagEnabled()) {
                //修改完先将下次执行时间置为空，等addOrModifyJob后会根据周期进行更改
                this.updateJobStatusExecution(Arrays.asList(entity), GlobalConsts.JobStatusExecution.NOT_RUN);
            }

        }

        //任务已禁用不修改任务，同时删除该job任务
        if (!entity.getFlagEnabled()) {
            try {
                this.deleteJob(entity.getId(), false);
            } catch (Exception e) {
            }
            return  true;
        };



        addOrModifyJob(entity);
        return true;
    }

    @Override
    public boolean removeCascadeByIds(Collection<? extends Serializable> idList) {

        for (Serializable id : idList) {
            deleteJob((String) id,true);
        }
        return true;
    }

    @Override
    public String runOnce(ScheduleJob scheduleJob) {

        String retMsg = "";
        String lastStatus = "1";
        String lastRemark = "";
        boolean isError = false;
        //校验任务状态
        this.checkJob(scheduleJob, "执行");
        Log logInfo=new Log();
        logInfo.setParam("任务名称："+scheduleJob.getJobName()+"，调用方法："+scheduleJob.getInvokeTarget()).setMsg("执行成功").setCreatorName(UserUtils.currentUser().getName())
                .setBusType(scheduleJob.getJobName()+ "[手动]").setBusId(scheduleJob.getId())
        ;

        try {

            //先删除任务
            try {
                this.deleteJob(scheduleJob.getId(), false);
            } catch (Exception e) {
            }

            String invokeTarget = scheduleJob.getInvokeTarget();
            if (StringUtils.isBlank(invokeTarget))
                throw new BusinessException("invokeTarget格式错误，invokeTarget=【%s】", invokeTarget);
            String[] invokeTargetArr = invokeTarget.split("\\.");
            if (invokeTargetArr == null || invokeTargetArr.length != 2)
                throw new BusinessException("invokeTarget格式错误(示例：serviceName.fun())，invokeTarget=【%s】", invokeTarget);

            String funName = invokeTargetArr[1].replace("()", "");
            if (funName.indexOf("(") != -1) funName = funName.substring(0, funName.indexOf("("));


            //更新任务状态
            this.updateJobStatusExecution(Arrays.asList(scheduleJob), GlobalConsts.JobStatusExecution.EXEC);
            logInfo.setLogType(LogType.success);
            Object objRet = SpringUtils.invokeMethod(invokeTargetArr[0], funName, StringUtils.getFunParam(invokeTargetArr[1]));
            if (objRet != null) {
                if (objRet instanceof RestResult) {
                    RestResult restResult = (RestResult) objRet;
                    if (!restResult.isSuccess()) {
                        logInfo.setLogType(LogType.error);
                    }
                   try {
                       log.error("restResult {}", JSON.toJSONString(restResult));
                   }catch (Exception e) {
                       log.error("restResult ERROR", e);
                   }
                    BusinessException.throwErrorIf(!restResult.isSuccess(),restResult.getMsg());
                    retMsg = restResult.getMsg();
                } else {
                    retMsg = objRet+"";
                }
            }
            if (UtilCommon.strIsNull(retMsg)) retMsg = "该任务未返回任何信息，请查看系统日志";
            lastRemark = retMsg;

            addOrModifyJob(scheduleJob);
            logInfo.setMsg(retMsg);
        }  catch (Exception e) {
            lastStatus = "0";
            lastRemark = e.getMessage();
            isError = true;
            String errorKey = "runOnce.error."+scheduleJob.getJobName()+"."+System.currentTimeMillis();
            log.error(errorKey,e);
            //保存日志，更新任务状态
            String traceMsg = UtilJavax.getStackTrace(e);
            if (traceMsg.length() > 300) traceMsg= traceMsg.substring(300);
            logInfo.setLogType(LogType.error).setMsg("执行失败"+",日志搜索标识，"+errorKey+"，异常信息："+ traceMsg);

            //推送消息给运营管理员 和管理员
            CommonService.pushMessageRole("定时任务执行失败:"+scheduleJob.getJobName(), traceMsg, "admin_yy,admin");
            throw new BusinessException("[%s]运行失败，原因：%s", scheduleJob.getJobName(), e.getMessage());
        } finally {
            //保存日志，更新任务状态
            Date dateNow = DateUtils.now();
            scheduleJob.setLastDate(dateNow).setLastStatus(lastStatus).setLastRemark(lastRemark).setStatusExecution(GlobalConsts.JobStatusExecution.NOT_EXEC);
            if (!isError) { //执行成功
                scheduleJob.setLastSuccessDate(dateNow);
            }
            this.updateById(scheduleJob);

            try {
                logService.save(logInfo);
            }catch (Exception e) {

            }
        }
        return retMsg;
    }

    @Override
    public void pause(ScheduleJob scheduleJob) {
        try {

            //校验任务状态
            this.checkJob(scheduleJob,"暂停");

            TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
            Trigger trigger = this.scheduler.getScheduler().getTrigger(triggerKey);
            if (trigger == null) throw new BusinessException("该任务尚未启用，请先启用该任务，然后重新启动定时器");
            this.scheduler.getScheduler().pauseJob(trigger.getJobKey());
            this.scheduler.getScheduler().pauseTrigger(trigger.getKey());

            addOrModifyJob(scheduleJob);

            //更新任务状态
            this.updateJobStatusExecution(Arrays.asList(scheduleJob), GlobalConsts.JobStatusExecution.PAUSE);
        } catch (SchedulerException e) {
            throw new BusinessException("暂停作业失败", e);
        }
    }

    @Override
    public void resume(ScheduleJob scheduleJob) {
        try {
            //校验任务状态
            this.checkJob(scheduleJob,"恢复");

            TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
            Trigger trigger = this.scheduler.getScheduler().getTrigger(triggerKey);
            if (trigger == null) throw new BusinessException("该任务尚未启用，请先启用该任务，然后重新启动定时器");
            this.scheduler.getScheduler().resumeJob(trigger.getJobKey());
            this.scheduler.getScheduler().resumeTrigger(trigger.getKey());

            //更新任务状态
            this.updateJobStatusExecution(Arrays.asList(scheduleJob), GlobalConsts.JobStatusExecution.NOT_EXEC);
        } catch (SchedulerException e) {
            throw new BusinessException("恢复作业失败", e);
        }
    }

    @Override
    public List<String> fiveCronTime(String cronExpression) {
        List<String> list = new ArrayList<>();
        CronTriggerImpl cronTriggerImpl = new CronTriggerImpl();
        try {
            cronTriggerImpl.setCronExpression(cronExpression);
        } catch(ParseException e) {
            e.printStackTrace();
        }
        List<Date> dates = TriggerUtils.computeFireTimes(cronTriggerImpl, null, 10);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (Date date : dates) {
            list.add(dateFormat.format(date));
        }
        return list;
    }

    private static boolean isRunning =  false;
    @Override
    public void startAll(boolean throwE) {
        if (isRunning()) {
            if (throwE) throw new BusinessException("启动作业调度器失败,原因：定时器已启动，请先停止再执行启动");
            return;
        }

        logger.info("#########scheduleJobService.startAll 启动定时器#########");

        try {
            this.update(Wrappers.<ScheduleJob>update().set("status_execution", GlobalConsts.JobStatusExecution.NOT_RUN));

            List<ScheduleJob> scheduleJobs = this.list(Wrappers.<ScheduleJob>query().eq("flag_enabled",1));
            if (scheduleJobs == null || scheduleJobs.size() < 1) if (throwE) throw  new BusinessException("当前没有启用的定时任务");
            for (ScheduleJob scheduleJob : scheduleJobs) {
                this.addOrModifyJob(scheduleJob);
            }

            this.scheduler.setStartupDelay(0);
            this.scheduler.start();
            //更新任务状态
            this.updateJobStatusExecution(scheduleJobs, GlobalConsts.JobStatusExecution.NOT_EXEC);
            isRunning = true;
            logger.info("#########scheduleJobService.startAll 启动定时器成功#########");
        } catch (Exception e) {

            if (throwE)  {
                logger.error("#########scheduleJobService.startAll 启动定时器失败#########",e);
                throw new BusinessException("启动作业调度器失败,原因："+e.getMessage(), e);
            }else {
                logger.info("#########scheduleJobService.startAll 启动定时器失败#########",e);
            }
        }
    }

    @Override
    public boolean stopAll(boolean throwE) {
        try {
            //不抛出异常为startservice调用
            if (!throwE) {
                //更新任务状态
                this.updateJobStatusExecution(this.list(), GlobalConsts.JobStatusExecution.NOT_RUN);
                return true;
            }
            //非执行的
            List<ScheduleJob> scheduleJobList = this.list(Wrappers.<ScheduleJob>query().ne("status_execution",GlobalConsts.JobStatusExecution.EXEC));
            if (scheduleJobList != null && scheduleJobList.size() >0){
                scheduleJobList.stream().forEach(scheduleJob -> {
                    this.deleteJob(scheduleJob.getId(),false);
                });

                //更新任务状态
                this.updateJobStatusExecution(scheduleJobList, GlobalConsts.JobStatusExecution.NOT_RUN);
            }

            //执行中
            scheduleJobList = this.list(Wrappers.<ScheduleJob>query().eq("status_execution",GlobalConsts.JobStatusExecution.EXEC));
            if (scheduleJobList != null && scheduleJobList.size() >0){
                List<String> jobNameList = scheduleJobList.stream().map(ScheduleJob::getJobName).collect(Collectors.toList());
                if (throwE) {
                    throw new BusinessException("停止作业调度器失败,请稍候重试，以下任务业务尚在处理中！<br>"+ StringUtils.join(jobNameList,","));
                }
            }else {
                this.scheduler.stop();
                isRunning = false;
            }
            return true;
        } catch (SchedulingException e) {

            if (throwE) {
                log.error("停止作业调度器失败", e);
                throw new BusinessException("停止作业调度器失败,原因："+e.getMessage(), e);
            } else {
                log.info("停止作业调度器失败", e);
            }
            return false;
        }
    }

    @Override
    public boolean isRunning() {
        try {
            return !this.scheduler.getScheduler().isInStandbyMode() && this.scheduler.getScheduler().isStarted() && isRunning;
        } catch (SchedulerException e) {
            log.error("获取作业调度器状态失败", e);
            return false;
        }
    }

    @Override
    public void deleteJob(String id,boolean isDeleteDate) {
        try {
            ScheduleJob job = this.getById(id);
            if (job == null) {
                return;
            }

            Scheduler sched = scheduler.getScheduler();

            TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());

            // 停止触发器
            sched.pauseTrigger(triggerKey);
            // 移除触发器
            sched.unscheduleJob(triggerKey);
            // 删除任务
            sched.deleteJob(JobKey.jobKey(job.getJobName(), job.getJobGroup()));

            if (isDeleteDate) {
                this.removeById(id);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public void addOrModifyJob(ScheduleJob job) {

        String invokeTarget = job.getInvokeTarget();
        if (StringUtils.isBlank(invokeTarget)) {
            throw new BusinessException("目标字符串 不能为空");
        }

        try {
            JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
            JobDetail jobDetail = scheduler.getScheduler().getJobDetail(jobKey);


            Class<? extends Job> jobClass;

            if (SpringBeanMethodInvokeTask.checkValidation(invokeTarget)) {
                jobClass = SpringBeanMethodInvokeTask.class;
            } else if (StaticMethodInvokeTask.checkValidation(invokeTarget)) {
                jobClass = StaticMethodInvokeTask.class;
            } else {
                throw new BusinessException(MessageFormat.format("目标字符串 {0} 参数格式错误", invokeTarget));
            }

            //misfire策略
            CronScheduleBuilder scheduleBuilder;
            Integer misfireInstruction = job.getMisfireInstruction();
            if (String.valueOf(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING).equals(misfireInstruction)) {
                scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()).withMisfireHandlingInstructionDoNothing();
            } else {
                scheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()).withMisfireHandlingInstructionFireAndProceed();
            }
            CronTrigger trigger =
                    TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup())
                            .withSchedule(scheduleBuilder).build();
            //如果存在对应的Job，修改。否则增加一个Job
            if (jobDetail != null) {
                modifyJobCronExpression(job);
            } else {
                jobDetail = JobBuilder.newJob(jobClass).withIdentity(job.getJobName(), job.getJobGroup()).usingJobData(new JobDataMap(Collections.singletonMap(ScheduleJob.class.getName(), job))).build();
                scheduler.getScheduler().scheduleJob(jobDetail, trigger);
            }

            TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
            Trigger trigger2 = this.scheduler.getScheduler().getTrigger(triggerKey);
            this.updateFields(
                    "sys_schedule_job",
                    job.getId(),
                    Arrays.asList("next_fire_time"),
                    Arrays.asList(trigger2 != null ? DateUtils.format(trigger2.getNextFireTime(),"yyyy-MM-dd HH:mm:ss") : null),
                    true
            );
        } catch (SchedulerException e) {
            throw new BusinessException("添加或修改作业失败", e);
        }
    }

    private void modifyJobCronExpression(ScheduleJob job) {
        try {
            scheduler.getScheduler().getJobDetail(new JobKey(job.getJobName(), job.getJobGroup())).getJobDataMap().put(ScheduleJob.class.getName(), job);

            TriggerKey triggerKey = scheduler.getScheduler().getTriggersOfJob(new JobKey(job.getJobName(),
                    job.getJobGroup())).get(0).getKey();
            CronTriggerImpl triggerCron;
            triggerCron = (CronTriggerImpl) scheduler.getScheduler().getTrigger(triggerKey);
            triggerCron.setCronExpression(job.getCronExpression());


            scheduler.getScheduler().rescheduleJob(new TriggerKey(job.getJobName(),
                    job.getJobGroup()), triggerCron);
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    //更新定时任务状态
    @Override
    public void updateJobStatusExecution(List<ScheduleJob> scheduleJobs, String toStatus) {
        if (scheduleJobs == null || scheduleJobs.size() < 1) return;
        List<String> jobIdList = scheduleJobs.stream().map(ScheduleJob::getId).collect(Collectors.toList());


        UpdateWrapper updateWrapper = Wrappers.<ScheduleJob>update();
        updateWrapper.in("id",jobIdList);
        updateWrapper.set("status_execution", toStatus);
        updateWrapper.set("status_execution_date", DateUtils.now());
        //停止时将下次执行时间置为空
        if (toStatus.equals(GlobalConsts.JobStatusExecution.NOT_RUN)) {
            updateWrapper.setSql("next_fire_time = null");
        } else {
            updateWrapper.ne("status_execution", toStatus);
        }
        this.update(updateWrapper);
    }


    //任务操作前执行校验
    @Override
    public void checkJob(ScheduleJob scheduleJob, String opType) {
        ScheduleJob entityDb=this.getById(scheduleJob.getId());
        if (GlobalConsts.JobStatusExecution.NOT_RUN.equals(entityDb.getStatusExecution()))  {
            try {
                this.deleteJob(entityDb.getId(), false);
            }catch ( Exception e) {
            }
            throw new BusinessException("[%s}[%s}失败，定时器没有启动",scheduleJob.getJobName(),opType);
        }

        if (GlobalConsts.JobStatusExecution.EXEC.equals(entityDb.getStatusExecution()))  {
            throw new BusinessException("[%s}[%s}失败，该任务业务处理中，请稍候再尝试",scheduleJob.getJobName(),opType);
        }

        if (!entityDb.getFlagEnabled())  {
            try {
                this.deleteJob(entityDb.getId(), false);
            }catch ( Exception e) {
            }
            throw new BusinessException("[%s}[%s}失败，该任务已被禁用",scheduleJob.getJobName(),opType);
        };
    }


    //任务操作前执行校验
    @Override
    public String testJob() {
        log.info("testJob");
        return "testJob执行结束";
    }

    /**
     * @description: 获取某个调用目标对应的任务的最后执行时间，如果不存在则返回 2000-01-01 00:00:00
     **/
    @Override
    public String getLastSuccessDate(String invokeTarget) {
        ScheduleJob scheduleJob = this.getOne(Wrappers.<ScheduleJob>query().
                eq("invoke_target",invokeTarget), false);
        BusinessException.throwValidFailIf(scheduleJob == null,"未找到相关任务，调用目标: %s",invokeTarget);
        if (scheduleJob.getLastSuccessDate() == null) {
            return "2000-01-01 00:00:00";
        }
        return  DateUtils.format(scheduleJob.getLastSuccessDate(), "yyyy-MM-dd HH:mm:ss");
    }
}
